/*
 * Javassist, a Java-bytecode translator toolkit.
 * Copyright (C) 1999- Shigeru Chiba. All Rights Reserved.
 *
 * The contents of this file are subject to the Mozilla Public License Version
 * 1.1 (the "License"); you may not use this file except in compliance with
 * the License.  Alternatively, the contents of this file may be used under
 * the terms of the GNU Lesser General Public License Version 2.1 or later,
 * or the Apache License Version 2.0.
 *
 * Software distributed under the License is distributed on an "AS IS" basis,
 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
 * for the specific language governing rights and limitations under the
 * License.
 */

package com.feilong.lib.javassist.tools.reflect;

import com.feilong.lib.javassist.CannotCompileException;
import com.feilong.lib.javassist.ClassPool;
import com.feilong.lib.javassist.CodeConverter;
import com.feilong.lib.javassist.CtClass;
import com.feilong.lib.javassist.CtField;
import com.feilong.lib.javassist.CtMethod;
import com.feilong.lib.javassist.CtMethod.ConstParameter;
import com.feilong.lib.javassist.CtNewMethod;
import com.feilong.lib.javassist.Modifier;
import com.feilong.lib.javassist.NotFoundException;
import com.feilong.lib.javassist.Translator;
import com.feilong.lib.javassist.bytecode.BadBytecode;
import com.feilong.lib.javassist.bytecode.ClassFile;
import com.feilong.lib.javassist.bytecode.MethodInfo;

/**
 * The class implementing the behavioral reflection mechanism.
 *
 * <p>
 * If a class is reflective,
 * then all the method invocations on every
 * instance of that class are intercepted by the runtime
 * metaobject controlling that instance. The methods inherited from the
 * super classes are also intercepted except final methods. To intercept
 * a final method in a super class, that super class must be also reflective.
 *
 * <p>
 * To do this, the original class file representing a reflective class:
 *
 * <pre>
 * class Person{
 * 
 *     public int f(int i){
 *         return i + 1;
 *     }
 * 
 *     public int value;
 * }
 * </pre>
 *
 * <p>
 * is modified so that it represents a class:
 *
 * <pre>
 * class Person implements Metalevel {
 *   public int _original_f(int i) { return i + 1; }
 *   public int f(int i) { <i>delegate to the metaobject</i> }
 *
 *   public int value;
 *   public int _r_value() { <i>read "value"</i> }
 *   public void _w_value(int v) { <i>write "value"</i> }
 *
 *   public ClassMetaobject _getClass() { <i>return a class metaobject</i> }
 *   public Metaobject _getMetaobject() { <i>return a metaobject</i> }
 *   public void _setMetaobject(Metaobject m) { <i>change a metaobject</i> }
 * }
 * </pre>
 *
 * @see com.feilong.lib.javassist.tools.reflect.ClassMetaobject
 * @see com.feilong.lib.javassist.tools.reflect.Metaobject
 * @see com.feilong.lib.javassist.tools.reflect.Loader
 */
public class Reflection implements Translator{

    static final String     classobjectField         = "_classobject";

    static final String     classobjectAccessor      = "_getClass";

    static final String     metaobjectField          = "_metaobject";

    static final String     metaobjectGetter         = "_getMetaobject";

    static final String     metaobjectSetter         = "_setMetaobject";

    static final String     readPrefix               = "_r_";

    static final String     writePrefix              = "_w_";

    static final String     metaobjectClassName      = com.feilong.lib.javassist.tools.reflect.Metaobject.class.getName();

    static final String     classMetaobjectClassName = com.feilong.lib.javassist.tools.reflect.ClassMetaobject.class.getName();

    protected CtMethod      trapMethod, trapStaticMethod;

    protected CtMethod      trapRead, trapWrite;

    protected CtClass[]     readParam;

    protected ClassPool     classPool;

    protected CodeConverter converter;

    private boolean isExcluded(String name){
        return name.startsWith(ClassMetaobject.methodPrefix) || name.equals(classobjectAccessor) || name.equals(metaobjectSetter)
                        || name.equals(metaobjectGetter) || name.startsWith(readPrefix) || name.startsWith(writePrefix);
    }

    /**
     * Constructs a new <code>Reflection</code> object.
     */
    public Reflection(){
        classPool = null;
        converter = new CodeConverter();
    }

    /**
     * Initializes the object.
     */
    @Override
    public void start(ClassPool pool) throws NotFoundException{
        classPool = pool;
        String classname = "com.feilong.lib.javassist.tools.reflect.Sample";
        try{
            CtClass c = classPool.get(classname);
            rebuildClassFile(c.getClassFile());
            trapMethod = c.getDeclaredMethod("trap");
            trapStaticMethod = c.getDeclaredMethod("trapStatic");
            trapRead = c.getDeclaredMethod("trapRead");
            trapWrite = c.getDeclaredMethod("trapWrite");
            readParam = new CtClass[] { classPool.get("java.lang.Object") };
        }catch (NotFoundException e){
            throw new RuntimeException(classname + "  is not found or broken.");
        }catch (BadBytecode e){
            throw new RuntimeException(classname + "  is not found or broken.");
        }
    }

    /**
     * Inserts hooks for intercepting accesses to the fields declared
     * in reflective classes.
     */
    @Override
    public void onLoad(ClassPool pool,String classname) throws CannotCompileException,NotFoundException{
        CtClass clazz = pool.get(classname);
        clazz.instrument(converter);
    }

    /**
     * Produces a reflective class.
     * If the super class is also made reflective, it must be done
     * before the sub class.
     *
     * @param classname
     *            the name of the reflective class
     * @param metaobject
     *            the class name of metaobjects.
     * @param metaclass
     *            the class name of the class metaobject.
     * @return <code>false</code> if the class is already reflective.
     *
     * @see com.feilong.lib.javassist.tools.reflect.ClassMetaobject
     */
    public boolean makeReflective(String classname,String metaobject,String metaclass) throws CannotCompileException,NotFoundException{
        return makeReflective(classPool.get(classname), classPool.get(metaobject), classPool.get(metaclass));
    }

    /**
     * Produces a reflective class.
     * If the super class is also made reflective, it must be done
     * before the sub class.
     *
     * @param clazz
     *            the reflective class.
     * @param metaobject
     *            the class of metaobjects.
     *            It must be a subclass of
     *            <code>Metaobject</code>.
     * @param metaclass
     *            the class of the class metaobject.
     *            It must be a subclass of
     *            <code>ClassMetaobject</code>.
     * @return <code>false</code> if the class is already reflective.
     *
     * @see com.feilong.lib.javassist.tools.reflect.ClassMetaobject
     */
    public boolean makeReflective(Class<?> clazz,Class<?> metaobject,Class<?> metaclass) throws CannotCompileException,NotFoundException{
        return makeReflective(clazz.getName(), metaobject.getName(), metaclass.getName());
    }

    /**
     * Produces a reflective class. It modifies the given
     * <code>CtClass</code> object and makes it reflective.
     * If the super class is also made reflective, it must be done
     * before the sub class.
     *
     * @param clazz
     *            the reflective class.
     * @param metaobject
     *            the class of metaobjects.
     *            It must be a subclass of
     *            <code>Metaobject</code>.
     * @param metaclass
     *            the class of the class metaobject.
     *            It must be a subclass of
     *            <code>ClassMetaobject</code>.
     * @return <code>false</code> if the class is already reflective.
     *
     * @see com.feilong.lib.javassist.tools.reflect.ClassMetaobject
     */
    public boolean makeReflective(CtClass clazz,CtClass metaobject,CtClass metaclass)
                    throws CannotCompileException,CannotReflectException,NotFoundException{
        if (clazz.isInterface()){
            throw new CannotReflectException("Cannot reflect an interface: " + clazz.getName());
        }

        if (clazz.subclassOf(classPool.get(classMetaobjectClassName))){
            throw new CannotReflectException("Cannot reflect a subclass of ClassMetaobject: " + clazz.getName());
        }

        if (clazz.subclassOf(classPool.get(metaobjectClassName))){
            throw new CannotReflectException("Cannot reflect a subclass of Metaobject: " + clazz.getName());
        }

        registerReflectiveClass(clazz);
        return modifyClassfile(clazz, metaobject, metaclass);
    }

    /**
     * Registers a reflective class. The field accesses to the instances
     * of this class are instrumented.
     */
    private void registerReflectiveClass(CtClass clazz){
        CtField[] fs = clazz.getDeclaredFields();
        for (CtField f : fs){
            int mod = f.getModifiers();
            if ((mod & Modifier.PUBLIC) != 0 && (mod & Modifier.FINAL) == 0){
                String name = f.getName();
                converter.replaceFieldRead(f, clazz, readPrefix + name);
                converter.replaceFieldWrite(f, clazz, writePrefix + name);
            }
        }
    }

    private boolean modifyClassfile(CtClass clazz,CtClass metaobject,CtClass metaclass) throws CannotCompileException,NotFoundException{
        if (clazz.getAttribute("Reflective") != null){
            return false; // this is already reflective.
        }
        clazz.setAttribute("Reflective", new byte[0]);

        CtClass mlevel = classPool.get(com.feilong.lib.javassist.tools.reflect.Metalevel.class.getName());
        boolean addMeta = !clazz.subtypeOf(mlevel);
        if (addMeta){
            clazz.addInterface(mlevel);
        }

        processMethods(clazz, addMeta);
        processFields(clazz);

        CtField ctField;
        if (addMeta){
            ctField = new CtField(classPool.get(com.feilong.lib.javassist.tools.reflect.Metaobject.class.getName()), metaobjectField, clazz);
            ctField.setModifiers(Modifier.PROTECTED);
            clazz.addField(ctField, CtField.Initializer.byNewWithParams(metaobject));

            clazz.addMethod(CtNewMethod.getter(metaobjectGetter, ctField));
            clazz.addMethod(CtNewMethod.setter(metaobjectSetter, ctField));
        }

        ctField = new CtField(classPool.get(com.feilong.lib.javassist.tools.reflect.ClassMetaobject.class.getName()), classobjectField, clazz);
        ctField.setModifiers(Modifier.PRIVATE | Modifier.STATIC);
        clazz.addField(ctField, CtField.Initializer.byNew(metaclass, new String[] { clazz.getName() }));

        clazz.addMethod(CtNewMethod.getter(classobjectAccessor, ctField));
        return true;
    }

    private void processMethods(CtClass clazz,boolean dontSearch) throws CannotCompileException,NotFoundException{
        CtMethod[] ms = clazz.getMethods();
        for (int i = 0; i < ms.length; ++i){
            CtMethod m = ms[i];
            int mod = m.getModifiers();
            if (Modifier.isPublic(mod) && !Modifier.isAbstract(mod)){
                processMethods0(mod, clazz, m, i, dontSearch);
            }
        }
    }

    private void processMethods0(int mod,CtClass clazz,CtMethod m,int identifier,boolean dontSearch)
                    throws CannotCompileException,NotFoundException{
        CtMethod body;
        String name = m.getName();

        if (isExcluded(name)){
            return; // from a reflective class.
        }

        CtMethod ctMethod;
        if (m.getDeclaringClass() == clazz){
            if (Modifier.isNative(mod)){
                return;
            }

            ctMethod = m;
            if (Modifier.isFinal(mod)){
                mod &= ~Modifier.FINAL;
                ctMethod.setModifiers(mod);
            }
        }else{
            if (Modifier.isFinal(mod)){
                return;
            }

            mod &= ~Modifier.NATIVE;
            ctMethod = CtNewMethod.delegator(findOriginal(m, dontSearch), clazz);
            ctMethod.setModifiers(mod);
            clazz.addMethod(ctMethod);
        }

        ctMethod.setName(ClassMetaobject.methodPrefix + identifier + "_" + name);

        if (Modifier.isStatic(mod)){
            body = trapStaticMethod;
        }else{
            body = trapMethod;
        }

        CtMethod wmethod = CtNewMethod.wrapped(
                        m.getReturnType(),
                        name,
                        m.getParameterTypes(),
                        m.getExceptionTypes(),
                        body,
                        ConstParameter.integer(identifier),
                        clazz);
        wmethod.setModifiers(mod);
        clazz.addMethod(wmethod);
    }

    private CtMethod findOriginal(CtMethod m,boolean dontSearch) throws NotFoundException{
        if (dontSearch){
            return m;
        }

        String name = m.getName();
        CtMethod[] ms = m.getDeclaringClass().getDeclaredMethods();
        for (CtMethod element : ms){
            String orgName = element.getName();
            if (orgName.endsWith(name) && orgName.startsWith(ClassMetaobject.methodPrefix)
                            && element.getSignature().equals(m.getSignature())){
                return element;
            }
        }

        return m;
    }

    private void processFields(CtClass clazz) throws CannotCompileException,NotFoundException{
        CtField[] fs = clazz.getDeclaredFields();
        for (CtField f : fs){
            int mod = f.getModifiers();
            if ((mod & Modifier.PUBLIC) != 0 && (mod & Modifier.FINAL) == 0){
                mod |= Modifier.STATIC;
                String name = f.getName();
                CtClass ftype = f.getType();
                CtMethod wmethod = CtNewMethod
                                .wrapped(ftype, readPrefix + name, readParam, null, trapRead, ConstParameter.string(name), clazz);
                wmethod.setModifiers(mod);
                clazz.addMethod(wmethod);
                CtClass[] writeParam = new CtClass[2];
                writeParam[0] = classPool.get("java.lang.Object");
                writeParam[1] = ftype;
                wmethod = CtNewMethod.wrapped(
                                CtClass.voidType,
                                writePrefix + name,
                                writeParam,
                                null,
                                trapWrite,
                                ConstParameter.string(name),
                                clazz);
                wmethod.setModifiers(mod);
                clazz.addMethod(wmethod);
            }
        }
    }

    public void rebuildClassFile(ClassFile cf) throws BadBytecode{
        if (ClassFile.MAJOR_VERSION < ClassFile.JAVA_6){
            return;
        }

        for (MethodInfo mi : cf.getMethods()){
            mi.rebuildStackMap(classPool);
        }
    }
}
